feat: event-driven governance (sync + async event rules) (#230)#568
Draft
krokoko wants to merge 6 commits into
Draft
feat: event-driven governance (sync + async event rules) (#230)#568krokoko wants to merge 6 commits into
krokoko wants to merge 6 commits into
Conversation
…le-language question (#230) Aggregate ceiling rules were unreliable: - turn_count_gte rules never fired — the agent emitted `turn` but every evaluator reads `turn_count` (NaN → no match). Agent now also emits the cumulative aliases `turn_count` / `cumulative_cost_usd` that the rules read. - cost/turn aggregates were reconstructed from the current event only, so the per-session SDK reset (PR-fix retries, container restart) undercounted them. evaluateAsyncEventRules now persists a monotonic high-water mark on the TaskRecord (climb-only conditional UpdateItem) and evaluates against it, so ceilings survive restarts. Fanout's ad-hoc reconstruction is removed. Resolves the §10 rule-language open question: declarative field matchers are permanent; Cedar-on-events is rejected (Cedar is authorization, has no aggregation, and would add a third Cedar runtime for no gain). ponytail: high-water is max(session totals), not a cross-session SUM — upgrade path noted on the TaskRecord fields if PR-fix retries must accrue.
- rethrow retryable infra errors (throttle/5xx) from task load and high-water update so the stream retries instead of silently under-counting a cost/turn ceiling - seed the monotonic cost mark from authoritative task.cost_usd so a ceiling can trip on a non-cost event and the first eval of an already-costed task - normalize aggregate metadata aliases on both evaluators; drop the duplicate cumulative_* / turn_count aliases the progress writer emitted - default gate_on_event_async ts_module to the real task_state so a missing module pauses for approval instead of crashing - key get-event-rules cache on repo + workflow_ref (rules depend on the workflow's eventRulePack) - add coverage for the new retry/seed paths
Move the event-rules asset tree (schema, packs, parity fixtures) from contracts/ to agent/event-rules/, alongside agent/workflows/ — one asset tree, one authoring UX, and the natural source for the future RegistryService (#246). The agent runtime never loads these files (it receives resolved event_rules on the TaskRecord), so only the three file consumers move: the CDK pack resolver (bundle-time import), the CLI --fixture loader, and the Python + TS parity/evaluator tests. Dual-plane parity is unchanged — the CDK-resolved rules remain the single source both planes consume. Repoints resolver import, CLI fixture path, four test fixture paths, blueprint schema comment, and EVENT_GOVERNANCE.md (+ Starlight mirror).
Multi-agent review surfaced that the branch hardened three DB read/persist paths against throttles but left the actual enforcement writes swallowing them — defeating the ceiling/approval guarantees the branch existed to fix. - cancel_task and async require_approval now rethrow retryable infra errors (throttle/5xx) so the FanOut record retries instead of silently letting a task run past its cost ceiling / skip its approval gate; benign conditional failures on the approval path are classified and swallowed as INFO - lift isRetryableInfraError into shared retryable-error.ts (approval module consumes it without a circular import) - release the idempotency marker when an enforce action rethrows, so the retried record can re-claim and re-run rather than being deduped away - observe_only mode no longer fires notify/escalate side effects — gate the whole action block on enforce (was only guarding nudge/approval/cancel) - inject_nudge rethrows retryable errors too - parseEventRules (TS + Python) logs dropped malformed rules instead of silently vanishing a ceiling rule; Python now requires id AND on (no more KeyError on missing on) - add turn_count_gte to the inline BlueprintEventRule.when.aggregate type - drop severity 'critical' from schema + both type mirrors (the agent PolicyEngine only knows low/medium/high) - remove dead evaluation-filter branch in bgagent rules eval Tests: async require_approval (enforce + observe), inject_nudge truncation, idempotency dedup skip, cancel terminal-status guard, retryable-release-and- rethrow, unknown-pack 422, and a new event-rule-pack-resolver suite covering inline/pack merge + override. cdk 2297 pass, agent + cli green.
…om 2nd review - policy_decision emit is now inside the release-guarded try alongside the enforce action: a retryable throttle on the audit emit (which rethrows) used to leave the idempotency marker claimed but the action un-run, so the stream retry skipped the rule entirely — dropping a cost ceiling / approval gate silently. Emit + action now share one release-on-failure path. - cancelTaskByRule swallows a benign ConditionalCheckFailedException (task raced to terminal between load and the cancel Update) as INFO instead of rethrowing, mirroring the approval path — a CCF no longer parks the record in batchItemFailures forever. - claimIdempotency rethrows retryable infra errors instead of proceeding on an unclaimed marker, which risked a double notify/escalate on the eventual retry. Nits: comment get-event-rules cache as best-effort/per-instance; note that the CLI rules-eval matcher is a dry-run copy with the parity suites as source of truth. Tests: emit-failure-release, cancel CCF swallow (no release), claim-throttle rethrow. cdk 2300 pass, cli green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You can now govern autonomous agents by meaningful moments ("PR about to open," "cost exceeded $25," "milestone reached") — with rules that either block in real time or react after the fact, that survive container restarts, that can be safely dry-run first, and that are now authored alongside workflows ready for a shared registry.
Closes #230.
What this adds
A unified Event Governance layer. Before this, the only controls were tool-level Cedar policies (gate a bash command / file write). Operators think in moments, not tools — this adds declarative event rules (
when X happens → do Y) that fire on lifecycle/milestone/checkpoint/cost events.Each rule has:
on(event), optionalwhen(field matchers + aggregate conditions),action(require_approval/notify/escalate/cancel_task/inject_nudge/observe_only),mode(enforceorobserve_only), andevaluation(syncorasync).Two planes, one catalog:
Cross-language parity between the two evaluators is locked by shared fixtures tested on both sides. Precedence is explicit: tool Cedar hard-deny always wins; async never overrides a sync deny; every action is deduped by
(task_id, rule_id, correlation_id).Configuration lives on the repo Blueprint (inline rules or a pinned versioned pack). Rules are frozen onto the TaskRecord at submit time, so a task's governance can't shift under it mid-run.
CLI surface (
bgagent):rules list(resolved rules for a repo),rules eval --fixture(local dry-run); notifications/approvals flow through the existingwatch/pending/approve/denywith an[observe]tag for dry-run rules.Deferred (follow-ups, not in scope for this PR)
require_approvalon a still-RUNNING task flips the DB status but does not signal the running agent to pause (only the sync gate blocks in-process). The supported path today is post-hoc gates (e.g.pr_created). An in-agent poll would make async approval block a live task — worth a follow-up issue.approval_gate_capthe sync path enforces.on/when/aggregatematching but not theevaluationsync/async split or thepolicy_decisionmetadata builders — extending the shared fixtures would close that.EventRuleas a discriminated union (onaction) would make illegalmode/actioncombos unrepresentable rather than validated by convention.Testing
Will turn to ready once live tests are all done